Style Guide
This style guide ensures consistency and readability across the Investing Algorithm Framework codebase.
Python Style Guidelines
Code Formatting
We use Black for code formatting with the following configuration:
# pyproject.toml
[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
Import Style
Import Order
- Standard library imports
- Related third-party imports
- Local application/library imports
# Standard library
import logging
import os
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Union
# Third-party
import numpy as np
import pandas as pd
import ccxt
from sqlalchemy import Column, Integer, String
# Local
from investing_algorithm_framework.domain import TradingStrategy
from investing_algorithm_framework.infrastructure import CCXTOrderExecutor
from investing_algorithm_framework.services import PortfolioService
Import Guidelines
# Good: Explicit imports
from investing_algorithm_framework.domain import (
TradingStrategy,
BacktestDateRange,
PortfolioConfiguration
)
# Avoid: Wildcard imports
from investing_algorithm_framework.domain import *
# Good: Relative imports for same package
from .portfolio_service import PortfolioService
from ..domain.models import Order
# Avoid: Long relative imports
from ....some.deep.nested.module import SomeClass
Variable Naming
General Rules
# Good: Descriptive names
portfolio_total_value = 10000.0
moving_average_period = 20
btc_current_price = 45000.0
# Bad: Abbreviated or unclear names
ptv = 10000.0
map = 20
bcp = 45000.0
# Good: Boolean variables
is_backtesting = True
has_open_positions = False
should_rebalance = True
# Bad: Ambiguous boolean names
backtesting = True
positions = False
rebalance = True
Class Naming
# Good: PascalCase for classes
class TradingStrategy:
pass
class MovingAverageCrossoverStrategy(TradingStrategy):
pass
class CCXTDataProvider:
pass
# Bad: Snake_case or unclear names
class trading_strategy:
pass
class Strategy1:
pass
class MAStrat:
pass
Method and Function Naming
class PortfolioManager:
# Good: Verb-based method names
def calculate_total_value(self) -> float:
pass
def get_positions(self) -> List[Position]:
pass
def create_buy_order(self, symbol: str, amount: float) -> Order:
pass
def is_position_open(self, symbol: str) -> bool:
pass
# Bad: Unclear or noun-based names
def total(self) -> float:
pass
def positions(self) -> List[Position]:
pass
def order(self, symbol: str, amount: float) -> Order:
pass
Constants
# Good: UPPERCASE with underscores
DEFAULT_TIMEFRAME = "1d"
MAX_POSITION_SIZE = 0.25
API_RATE_LIMIT = 1200 # requests per minute
SUPPORTED_EXCHANGES = ["binance", "coinbase", "kraken"]
# Bad: Mixed case or unclear names
defaultTimeframe = "1d"
maxPos = 0.25
limit = 1200
exchanges = ["binance", "coinbase", "kraken"]
Type Hints
Basic Type Hints
from typing import Dict, List, Optional, Union
from datetime import datetime
def calculate_moving_average(
prices: List[float],
period: int
) -> float:
return sum(prices[-period:]) / period
def get_portfolio_value(
positions: Dict[str, float],
prices: Dict[str, float]
) -> Optional[float]:
if not positions:
return None
return sum(positions[symbol] * prices.get(symbol, 0)
for symbol in positions)
Complex Type Hints
from typing import Callable, TypeVar, Generic, Protocol
# Type variables
T = TypeVar('T')
StrategyType = TypeVar('StrategyType', bound='TradingStrategy')
# Generic classes
class DataProvider(Generic[T]):
def get_data(self, symbol: str) -> T:
pass
# Protocols for structural typing
class Tradeable(Protocol):
def get_current_price(self) -> float: ...
def create_order(self, side: str, amount: float) -> Order: ...
# Complex function signatures
def backtest_strategy(
strategy: TradingStrategy,
data_provider: DataProvider[pd.DataFrame],
date_range: BacktestDateRange,
callback: Optional[Callable[[BacktestRun], None]] = None
) -> BacktestResults:
pass
Documentation Style
Class Documentation
class MovingAverageStrategy(TradingStrategy):
"""
Trading strategy based on moving average crossover signals.
This strategy generates buy signals when a short-term moving average
crosses above a long-term moving average, and sell signals when the
short-term average crosses below the long-term average.
Attributes:
short_period: Number of periods for short moving average
long_period: Number of periods for long moving average
symbol: Trading symbol to apply strategy to
Example:
>>> strategy = MovingAverageStrategy(
... short_period=20,
... long_period=50,
... symbol="BTC/USDT"
... )
>>> app.add_strategy(strategy)
"""
def __init__(
self,
short_period: int = 20,
long_period: int = 50,
symbol: str = "BTC/USDT"
):
"""
Initialize the moving average strategy.
Args:
short_period: Period for short moving average. Must be less
than long_period. Defaults to 20.
long_period: Period for long moving average. Must be greater
than short_period. Defaults to 50.
symbol: Trading symbol to monitor. Defaults to "BTC/USDT".
Raises:
ValueError: If short_period >= long_period
ValueError: If periods are not positive integers
"""
if short_period >= long_period:
raise ValueError("Short period must be less than long period")
if short_period <= 0 or long_period <= 0:
raise ValueError("Periods must be positive integers")
self.short_period = short_period
self.long_period = long_period
self.symbol = symbol
Function Documentation
def calculate_sharpe_ratio(
returns: List[float],
risk_free_rate: float = 0.02,
periods_per_year: int = 252
) -> float:
"""
Calculate the Sharpe ratio for a series of returns.
The Sharpe ratio measures risk-adjusted return by comparing the excess
return of an investment over a risk-free rate to the volatility of the
investment.
Args:
returns: List of periodic returns (e.g., daily returns)
risk_free_rate: Annual risk-free rate. Defaults to 2% (0.02).
periods_per_year: Number of return periods per year. Defaults to
252 (trading days).
Returns:
The Sharpe ratio as a float. Higher values indicate better
risk-adjusted returns.
Raises:
ValueError: If returns list is empty
ValueError: If standard deviation is zero (no volatility)
Example:
>>> daily_returns = [0.01, -0.02, 0.015, 0.008, -0.005]
>>> sharpe = calculate_sharpe_ratio(daily_returns, 0.025)
>>> print(f"Sharpe Ratio: {sharpe:.2f}")
Sharpe Ratio: 1.25
Note:
This function assumes the returns are already in decimal format
(e.g., 0.01 for 1% return).
"""
if not returns:
raise ValueError("Returns list cannot be empty")
mean_return = sum(returns) / len(returns)
std_return = (sum((r - mean_return) ** 2 for r in returns) / len(returns)) ** 0.5
if std_return == 0:
raise ValueError("Cannot calculate Sharpe ratio with zero volatility")
# Annualize the Sharpe ratio
excess_return = mean_return - (risk_free_rate / periods_per_year)
annualized_excess_return = excess_return * periods_per_year
annualized_volatility = std_return * (periods_per_year ** 0.5)
return annualized_excess_return / annualized_volatility
Error Handling Style
Exception Hierarchy
# Base framework exception
class InvestingAlgorithmFrameworkError(Exception):
"""Base exception for all framework-related errors."""
pass
# Domain-specific exceptions
class TradingError(InvestingAlgorithmFrameworkError):
"""Base exception for trading-related errors."""
pass
class OrderError(TradingError):
"""Exception raised for order-related errors."""
pass
class InsufficientFundsError(OrderError):
"""Exception raised when insufficient funds for order."""
pass
class DataError(InvestingAlgorithmFrameworkError):
"""Exception raised for data-related errors."""
pass
class ConnectionError(DataError):
"""Exception raised for connection-related errors."""
pass
Exception Usage
def create_buy_order(
self,
symbol: str,
amount: float,
price: Optional[float] = None
) -> Order:
"""
Create a buy order for the specified symbol and amount.
Args:
symbol: Trading symbol (e.g., "BTC/USDT")
amount: Amount to buy in base currency
price: Limit price (optional, uses market price if None)
Returns:
Created order object
Raises:
ValueError: If amount is not positive
InsufficientFundsError: If account balance is insufficient
ConnectionError: If exchange connection fails
"""
# Input validation
if amount <= 0:
raise ValueError(f"Order amount must be positive, got {amount}")
if not symbol:
raise ValueError("Symbol cannot be empty")
try:
# Check available balance
balance = self._get_available_balance()
if balance < amount:
raise InsufficientFundsError(
f"Insufficient balance: {balance} < {amount}"
)
# Create order
order = self._create_order_on_exchange(symbol, amount, price)
return order
except ConnectionError as e:
# Re-raise connection errors with context
raise ConnectionError(
f"Failed to create order for {symbol}: {e}"
) from e
except Exception as e:
# Log unexpected errors and re-raise
logger.error(f"Unexpected error creating order: {e}", exc_info=True)
raise TradingError(f"Failed to create buy order: {e}") from e
Logging Style
Logger Setup
import logging
# Good: Module-level logger
logger = logging.getLogger(__name__)
class TradingStrategy:
def __init__(self):
# Good: Class-specific logger
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
def apply_strategy(self, algorithm, market_data):
self.logger.info("Executing trading strategy")
try:
current_price = market_data.get_last_price("BTC/USDT")
self.logger.debug(f"Current BTC price: ${current_price:.2f}")
# Strategy logic here
except Exception as e:
self.logger.error(f"Strategy execution failed: {e}", exc_info=True)
raise
Logging Messages
# Good: Informative log messages
logger.info("Starting portfolio rebalancing", extra={
'portfolio_value': portfolio.total_value,
'target_weights': target_weights,
'current_weights': current_weights
})
logger.warning(
"Position size exceeds risk limit",
extra={
'symbol': symbol,
'position_size': position_size,
'risk_limit': risk_limit,
'portfolio_percentage': position_size / portfolio.total_value
}
)
# Bad: Vague or missing information
logger.info("Rebalancing")
logger.warning("Risk limit exceeded")
# Good: Structured logging for production
logger.info("Order executed", extra={
'event_type': 'order_filled',
'order_id': order.id,
'symbol': order.symbol,
'side': order.side,
'amount': order.amount,
'price': order.price,
'timestamp': datetime.utcnow().isoformat()
})
Testing Style
Test Organization
# tests/test_trading_strategy.py
import pytest
from unittest.mock import Mock, patch
from investing_algorithm_framework import TradingStrategy
class TestTradingStrategy:
"""Test suite for TradingStrategy class."""
@pytest.fixture
def strategy(self):
"""Fixture providing a basic strategy instance."""
return TradingStrategy()
@pytest.fixture
def mock_market_data(self):
"""Fixture providing mock market data."""
mock_data = Mock()
mock_data.get_last_price.return_value = 50000.0
mock_data.get_data.return_value = [
{"open": 49000, "high": 51000, "low": 48000, "close": 50000}
]
return mock_data
def test_strategy_initialization(self, strategy):
"""Test strategy can be initialized with default parameters."""
assert strategy is not None
assert hasattr(strategy, 'apply_strategy')
def test_strategy_with_market_data(self, strategy, mock_market_data):
"""Test strategy execution with mock market data."""
algorithm = Mock()
# Should not raise exception
strategy.apply_strategy(algorithm, mock_market_data)
# Verify market data was accessed
mock_market_data.get_last_price.assert_called()
@pytest.mark.parametrize("price,expected_signal", [
(45000, "buy"),
(55000, "sell"),
(50000, "hold")
])
def test_strategy_signals_at_different_prices(
self,
strategy,
price,
expected_signal
):
"""Test strategy generates correct signals at different price levels."""
# Test implementation here
pass
def test_strategy_handles_missing_data(self, strategy):
"""Test strategy handles case when market data is unavailable."""
algorithm = Mock()
market_data = Mock()
market_data.get_last_price.return_value = None
# Should handle gracefully without crashing
strategy.apply_strategy(algorithm, market_data)
def test_strategy_error_handling(self, strategy):
"""Test strategy handles exceptions in market data access."""
algorithm = Mock()
market_data = Mock()
market_data.get_last_price.side_effect = ConnectionError("Network error")
with pytest.raises(ConnectionError):
strategy.apply_strategy(algorithm, market_data)
Assertion Style
# Good: Clear, specific assertions
def test_portfolio_value_calculation():
portfolio = Portfolio()
portfolio.add_position("BTC", 0.1, 50000)
portfolio.add_position("ETH", 2.0, 3000)
expected_value = (0.1 * 50000) + (2.0 * 3000)
actual_value = portfolio.calculate_total_value()
assert actual_value == expected_value
assert portfolio.get_position_count() == 2
# Good: Using pytest.approx for floating point comparisons
def test_sharpe_ratio_calculation():
returns = [0.01, -0.02, 0.015, 0.008, -0.005]
sharpe = calculate_sharpe_ratio(returns, risk_free_rate=0.02)
assert sharpe == pytest.approx(1.25, abs=0.01)
# Good: Testing exception messages
def test_invalid_order_amount():
portfolio = Portfolio()
with pytest.raises(ValueError, match="Order amount must be positive"):
portfolio.create_buy_order("BTC/USDT", -100)
Configuration Style
Environment Configuration
# config/development.py
class DevelopmentConfig:
"""Development environment configuration."""
DEBUG = True
TESTING = False
# Database
DATABASE_URI = "sqlite:///dev.db"
# Logging
LOG_LEVEL = "DEBUG"
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# Trading
PAPER_TRADING = True
INITIAL_BALANCE = 10000.0
# API Keys (should be loaded from environment)
EXCHANGE_API_KEY = os.getenv("DEV_EXCHANGE_API_KEY")
EXCHANGE_API_SECRET = os.getenv("DEV_EXCHANGE_API_SECRET")
# config/production.py
class ProductionConfig:
"""Production environment configuration."""
DEBUG = False
TESTING = False
# Database
DATABASE_URI = os.getenv("DATABASE_URI")
# Logging
LOG_LEVEL = "INFO"
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
# Trading
PAPER_TRADING = False
INITIAL_BALANCE = float(os.getenv("INITIAL_BALANCE", "1000"))
# Security
SECRET_KEY = os.getenv("SECRET_KEY")
EXCHANGE_API_KEY = os.getenv("EXCHANGE_API_KEY")
EXCHANGE_API_SECRET = os.getenv("EXCHANGE_API_SECRET")
YAML Configuration Style
# config.yaml
app:
name: "Trading Bot"
version: "1.0.0"
environment: "production"
database:
uri: "${DATABASE_URI}"
pool_size: 10
max_overflow: 20
exchanges:
binance:
api_key: "${BINANCE_API_KEY}"
api_secret: "${BINANCE_API_SECRET}"
sandbox: false
rate_limit: 1200
portfolio:
initial_balance: 10000.0
trading_symbol: "USDT"
max_position_size: 0.25
risk_per_trade: 0.02
strategies:
- name: "MA Crossover"
class: "MovingAverageStrategy"
parameters:
short_period: 20
long_period: 50
symbols: ["BTC/USDT", "ETH/USDT"]
logging:
level: "INFO"
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
handlers:
- type: "file"
filename: "logs/trading.log"
max_size: "10MB"
backup_count: 5
- type: "console"
stream: "stdout"
File Organization
Directory Structure
investing_algorithm_framework/
├── __init__.py
├── app/ # Application layer
│ ├── __init__.py
│ ├── app.py
│ ├── algorithm/
│ ├── analysis/
│ └── strategy/
├── domain/ # Domain models and business logic
│ ├── __init__.py
│ ├── models/
│ ├── services/
│ └── exceptions/
├── infrastructure/ # External integrations
│ ├── __init__.py
│ ├── exchanges/
│ ├── data_providers/
│ └── databases/
├── cli/ # Command-line interface
│ ├── __init__.py
│ └── commands/
└── utils/ # Utility functions
├── __init__.py
├── datetime_utils.py
└── math_utils.py
tests/
├── __init__.py
├── unit/
├── integration/
├── fixtures/
└── conftest.py
docs/
├── api/
├── guides/
└── examples/
examples/
├── basic_trading_bot.py
├── advanced_strategies/
└── data_analysis/
Module Structure
# investing_algorithm_framework/domain/models/order.py
"""
Order domain model.
This module contains the Order class and related enums for representing
trading orders within the framework.
"""
from enum import Enum
from datetime import datetime
from typing import Optional
from dataclasses import dataclass
__all__ = ['Order', 'OrderStatus', 'OrderSide', 'OrderType']
class OrderStatus(Enum):
"""Enumeration of possible order statuses."""
PENDING = "pending"
OPEN = "open"
FILLED = "filled"
CANCELLED = "cancelled"
REJECTED = "rejected"
class OrderSide(Enum):
"""Enumeration of order sides."""
BUY = "buy"
SELL = "sell"
class OrderType(Enum):
"""Enumeration of order types."""
MARKET = "market"
LIMIT = "limit"
STOP = "stop"
STOP_LIMIT = "stop_limit"
@dataclass
class Order:
"""
Represents a trading order.
This class encapsulates all information about a trading order,
including its type, status, and execution details.
Attributes:
id: Unique identifier for the order
symbol: Trading pair symbol (e.g., "BTC/USDT")
side: Order side (buy or sell)
type: Order type (market, limit, etc.)
amount: Order amount in base currency
price: Order price (None for market orders)
status: Current order status
created_at: Order creation timestamp
updated_at: Last update timestamp
"""
id: str
symbol: str
side: OrderSide
type: OrderType
amount: float
price: Optional[float] = None
status: OrderStatus = OrderStatus.PENDING
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
def __post_init__(self):
"""Initialize timestamps if not provided."""
if self.created_at is None:
self.created_at = datetime.utcnow()
if self.updated_at is None:
self.updated_at = self.created_at
Git Commit Style
Commit Message Format
<type>(<scope>): <description>
<body>
<footer>
Types
- feat: New feature
- fix: Bug fix
- docs: Documentation changes
- style: Code style changes (formatting, etc.)
- refactor: Code refactoring
- test: Adding or updating tests
- chore: Maintenance tasks
Examples
feat(portfolio): add automatic rebalancing functionality
Implement automatic portfolio rebalancing based on target weights.
The rebalancer monitors position sizes and triggers rebalancing
when positions deviate beyond the configured threshold.
Features:
- Configurable target weights per asset
- Customizable rebalancing threshold
- Support for scheduled rebalancing
- Comprehensive logging of rebalancing actions
Closes #123
fix(orders): handle exchange connection timeouts gracefully
Previously, connection timeouts during order placement would crash
the application. Now timeouts are caught and orders are retried
with exponential backoff.
- Add retry logic with exponential backoff
- Improve error messages for timeout scenarios
- Add comprehensive tests for timeout handling
Fixes #456
docs(api): update strategy creation examples
Update the strategy creation documentation to include examples
of the new portfolio rebalancing features and improved error
handling patterns.
test(integration): add end-to-end backtesting tests
Add comprehensive integration tests that verify the complete
backtesting workflow from strategy creation to results analysis.
- Test multiple strategies simultaneously
- Verify correct portfolio calculations
- Test error handling in edge cases
Following these style guidelines ensures that the codebase remains consistent, readable, and maintainable as the project grows and more contributors join the effort.